﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Net.Nntp.Cache
{
    using System;
    using System.Collections.Generic;
    using System.Data.SqlServerCe;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Reflection;
    using System.Text;
    using System.Threading;

    using Mews.Mime;
    using Mews.UnitTesting;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>Tests the <see cref="Cache"/> class.</summary>
    [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Not an issue in a test.")]
    [TestClass]
    public sealed class CacheTest : TestBase
    {
        private static readonly string ourDirectory =
            Path.GetDirectoryName(typeof(CacheTest).Assembly.ManifestModule.FullyQualifiedName);

        private const string OurGroupName = "our.group";

        /// <summary>Tests the <see cref="Cache"/> creation use cases.</summary>
        [TestMethod]
        public void CreationTest()
        {
            AssertThrow<ArgumentException>(() => new Cache("<", new NullCryptoProvider()).ToString());
            AssertThrow<ArgumentNullException>(() => new Cache(null, new NullCryptoProvider()).ToString());
            AssertThrow<FileNotFoundException>( // non-existent directory
                () => new Cache(Path.Combine(ourDirectory, GetUniqueString()), new NullCryptoProvider()).ToString());
            AssertThrow<FileNotFoundException>(
                () => new Cache(CreateUniqueDirectory(), new NullCryptoProvider()).ToString()); // non-existent DB file
            AssertThrow<ArgumentNullException>(() => new Cache(CreateUniqueDirectory(), null).ToString());

            AssertThrow<ArgumentException>(() => Cache.CreateCache(ourDirectory)); // Non-empty directory
            AssertThrow<ArgumentException>(() => Cache.CreateCache("*?"));
            AssertThrow<ArgumentNullException>(() => Cache.CreateCache(null));

            // sdf has the wrong format
            string dataDirectory2 = CreateUniqueDirectory();
            File.Create(Path.Combine(dataDirectory2, "Cache.sdf")).Dispose();
            AssertThrow<Exception>(() => new Cache(dataDirectory2, new NullCryptoProvider()).ToString());

            string tooLong = ourDirectory;

            while (tooLong.Length <= 248)
            {
                tooLong = Path.Combine(tooLong, GetUniqueString());
            }

            AssertThrow<IOException>(() => Cache.CreateCache(tooLong));

            using (var cache = CreateNewCache())
            {
                this.AddRandomHosts(cache);
            }
        }

        /// <summary>Tests auxiliary members of classes associated with <see cref="Cache"/>.</summary>
        [TestMethod]
        public void AuxiliaryMembersTest()
        {
            const int NewInboundArticlesReceiveLimit = 10;
            const int CachedInboundArticlesLimit = 10;

            using (var cache = CreateNewCache())
            {
                var host1 = CreateGmaneHost(cache);
                var group1 =
                    CreateBoostDeveloperGroup(host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);
                var group1Read = host1.GetGroups().First();
                var group2 = CreateGccDeveloperGroup(host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);

                var observer = new ConsoleObserver();
                cache.StartSendReceive(new IGroup[] { group1, group2 }, false, observer);

                EqualsGetHashCodeTest(group1, group1Read, group2);

                WaitForSendReceiveToFinish(cache, observer);
                observer.AssertNoExceptions();

                using (var query = group1.CreateQueryCache())
                {
                    EqualsGetHashCodeTest(
                        query.GetOriginalInboundArticles().First(),
                        query.GetOriginalInboundArticles().First(),
                        query.GetOriginalInboundArticles().ElementAt(1));
                    AssertReadStatus(query.GetOriginalInboundArticles(), false);
                    group1.MarkAllInboundArticlesAsRead();
                    AssertReadStatus(query.GetOriginalInboundArticles(), true);
                }
            }
        }

        /// <summary>Tests the main cache use cases with real servers.</summary>
        [TestMethod]
        public void MainTest()
        {
            const int NewInboundArticlesReceiveLimit = 30;
            const int CachedInboundArticlesLimit = 30;
            string dataDirectory;
            int group1ArticleCount;

            using (var cache = CreateNewCache())
            {
                dataDirectory = cache.DataDirectory;
                var host1 = CreateGmaneHost(cache);
                var group1 = CreateBoostDeveloperGroup(
                    host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);
                var group2 = CreateGccDeveloperGroup(host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);
                var host2 = CreateTestHost(cache);
                var group3 = CreateTestGroup(host2, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);

                TestStartStopSendReceive(cache, host1, group1, group3);
                TestOutboundArticles(cache, host2, group2, group3);
                TestQueryCache(NewInboundArticlesReceiveLimit, cache, group1, group2, group3);

                group2.Delete();
                Assert.AreEqual(host1.GetGroups().Count(), 1);
                host2.Delete();
                Assert.AreEqual(cache.GetHosts().Count(), 1);

                TestCompilerGeneratedEnumerator(cache.GetOutboundArticles(SendStatus.Sent).GetEnumerator());
                TestCompilerGeneratedEnumerator(cache.GetHosts().GetEnumerator());
                TestCompilerGeneratedEnumerator(host1.GetGroups().GetEnumerator());

                group1ArticleCount = GetArticleCount(group1);

                // Ensure that the next connection to the Cache will delete old articles
                group1.MarkAllInboundArticlesAsRead();
                group1.NewInboundArticlesReceiveLimit -= 2;
                group1.CachedInboundArticlesLimit -= 2;
            }

            TestCompaction(dataDirectory, group1ArticleCount);
        }

        /// <summary>Tests whether the data read from the DB is the same as the data stored in objects returned by Add*
        /// methods.</summary>
        [TestMethod]
        public void AddAndReadTest()
        {
            using (var cache = CreateNewCache())
            {
                var host = this.AddRandomHost(cache);
                var readHost = cache.GetHosts().First();
                Assert.AreEqual(host.Name, readHost.Name);
                Assert.AreEqual(host.Port, readHost.Port);
                Assert.AreEqual(host.UserName, readHost.UserName);
                Assert.AreEqual(host.EmailAddress, readHost.EmailAddress);
                Assert.AreEqual(host.AccountName, readHost.AccountName);
                Assert.AreEqual(host.Password, readHost.Password);

                var outboundArticle = host.AddOutboundArticle();
                var message = outboundArticle.LoadMessage();
                Assert.AreEqual(1, message.Fields.Count); // MIME-Version is always present
                Assert.IsNull(message.Body);
                var readOutboundArticle = cache.GetOutboundArticles(SendStatus.Draft).First();
                Assert.AreEqual(outboundArticle.SendStatus, readOutboundArticle.SendStatus);

                var group = this.AddRandomGroup(host);
                var readGroup = host.GetGroups().First();
                Assert.AreEqual(group.Name, readGroup.Name);
                Assert.AreEqual(group.DisplayName, readGroup.DisplayName);
                Assert.AreEqual(group.NewInboundArticlesReceiveLimit, readGroup.NewInboundArticlesReceiveLimit);
                Assert.AreEqual(group.CachedInboundArticlesLimit, readGroup.CachedInboundArticlesLimit);
                Assert.AreEqual(group.InboundArticleFilter, readGroup.InboundArticleFilter);
            }
        }

        /// <summary>Tests the case where a message has an In-Reply-To field but no References field. Moreover, the
        /// case is tested when an article is removed between header and body receive.</summary>
        [TestMethod]
        public void InReplyToTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var messageId1 = GetUniqueString();
                    var subject1 = GetUniqueString();
                    var messageId2 = GetUniqueString();

                    // Use overlong subject for second article
                    StringBuilder subject2Builder = new StringBuilder(256);

                    while (subject2Builder.Length <= 128)
                    {
                        subject2Builder.Append(GetUniqueString());
                    }

                    var subject2 = subject2Builder.ToString();

                    QueueSucceedingGroupCommand(server);
                    QueueSucceedingStatCommand(server, 1, messageId1);
                    QueueHeadCommand(server, 1, messageId1, subject1, null);
                    QueueSucceedingNextCommand(server, messageId2);
                    QueueHeadCommand(server, 2, messageId2, subject2, messageId1);
                    var observer = new ConsoleObserver();
                    var group = Receive(cache, false, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertNoExceptions();

                    using (var query = group.CreateQueryCache())
                    {
                        Assert.IsTrue(query.GetOriginalInboundArticles().Count() == 1);
                        var article1 = query.GetOriginalInboundArticles().First();
                        Assert.AreEqual(subject1, article1.Subject);
                        Assert.IsTrue(article1.GetFollowupInboundArticles().Count() == 1);
                        Assert.IsTrue(subject2.StartsWith(
                            article1.GetFollowupInboundArticles().First().Subject, StringComparison.Ordinal));

                        // Our FakeServer can only accept exactly one connection, we need to create a new one if we want
                        // to connect again
                        server.Dispose();

                        using (server = NntpFakeServerHelper.MakeAvailableServer(true))
                        {
                            // Test article removed between overview/header & body receive
                            article1.IsBeingWatched = true;

                            QueueSucceedingGroupCommand(server);
                            server.QueueCommand("BODY 1\r\n", "423 No article with that number\r\n");
                            cache.StartSendReceive(new IGroup[] { group }, false, observer);
                            WaitForSendReceiveToFinish(cache, observer);
                            observer.AssertNoExceptions();
                            Assert.IsTrue(query.GetOriginalInboundArticles().Count() == 1);
                            var article2 = query.GetOriginalInboundArticles().First();
                            Assert.IsTrue(subject2.StartsWith(article2.Subject, StringComparison.Ordinal));
                            Assert.IsTrue(article2.GetFollowupInboundArticles().Count() == 0);
                        }
                    }
                });
        }

        /// <summary>Tests what happens when the first message has been removed.</summary>
        [TestMethod]
        public void MissingFirstArticleTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var messageId1 = GetUniqueString();
                    var subject1 = GetUniqueString();
                    QueueSucceedingGroupCommand(server);
                    QueueFailingStatCommand(server, 1);
                    QueueSucceedingStatCommand(server, 2, messageId1);
                    QueueHeadCommand(server, 2, messageId1, subject1, null);
                    var observer = new ConsoleObserver();
                    var group = Receive(cache, false, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertNoExceptions();

                    using (var query = group.CreateQueryCache())
                    {
                        Assert.IsTrue(query.GetOriginalInboundArticles().Count() == 1);
                        var article1 = query.GetOriginalInboundArticles().First();
                        Assert.AreEqual(subject1, article1.Subject);
                        Assert.IsTrue(article1.GetFollowupInboundArticles().Count() == 0);
                    }
                });
        }

        /// <summary>Tests what happens when a subsequent message has been removed.</summary>
        [TestMethod]
        public void MissingSubsequentArticleTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var subject = GetUniqueString();
                    var group = GetGroupWithArticle(server, cache, GetUniqueString(), subject);

                    using (var query = group.CreateQueryCache())
                    {
                        Assert.IsTrue(query.GetOriginalInboundArticles().Count() == 1);
                        var article1 = query.GetOriginalInboundArticles().First();
                        Assert.AreEqual(subject, article1.Subject);
                        Assert.IsTrue(article1.GetFollowupInboundArticles().Count() == 0);
                    }
                });
        }

        /// <summary>Tests an empty group corner case.</summary>
        [TestMethod]
        public void EmptyGroupTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    server.QueueCommand("GROUP " + OurGroupName + "\r\n", "211 0 1 2 our.group\r\n");
                    var observer = new ConsoleObserver();
                    var group = Receive(cache, false, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertNoExceptions();
                });
        }

        /// <summary>Tests the case when a message-ID is reused (illegal). Moreover, the case is tested when the server
        /// responds with a different article number than was requested.</summary>
        [TestMethod]
        public void ReusedMessageIdTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var messageId = GetUniqueString();
                    var subject1 = GetUniqueString();
                    var subject2 = GetUniqueString();

                    QueueSucceedingGroupCommand(server);
                    QueueSucceedingStatCommand(server, 1, messageId);
                    QueueHeadCommand(server, 1, messageId, subject1, null);
                    QueueSucceedingNextCommand(server, messageId);
                    QueueHeadCommand(server, 2, messageId, subject2, null);
                    server.QueueCommand(
                        "BODY 1\r\n",
                        "222 2 " + messageId + " Body follows\r\nwhatever\r\n.\r\n");
                    var observer = new ConsoleObserver();
                    var group = Receive(cache, true, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertCallCount(new UnexpectedResponseException(), 1);

                    using (var query = group.CreateQueryCache())
                    {
                        Assert.IsTrue(query.GetOriginalInboundArticles().Count() == 1);
                        var article1 = query.GetOriginalInboundArticles().First();
                        Assert.AreEqual(subject1, article1.Subject);
                        Assert.IsTrue(article1.GetFollowupInboundArticles().Count() == 0);
                    }
                });
        }

        /// <summary>Tests what happens when a group is deleted before the cache had the chance to receive articles
        /// for it.</summary>
        [TestMethod]
        public void NonexistentGroupTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var observer = new ConsoleObserver();
                    var group = cache.AddHost("localhost", 119).AddGroup(OurGroupName, "Our Group", 100, 100);
                    group.Delete();
                    cache.StartSendReceive(new IGroup[] { group }, false, observer);
                    AssertThrow<TargetInvocationException>(() => WaitForSendReceiveToFinish(cache, observer));
                });
        }

        /// <summary>Tests what happens when a deleted article is being updated.</summary>
        [TestMethod]
        public void UpdateDeletedArticleTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var group = GetGroupWithArticle(server, cache, GetUniqueString(), GetUniqueString());

                    using (var query = group.CreateQueryCache())
                    {
                        var article = query.GetOriginalInboundArticles().First();
                        group.Delete();
                        AssertThrow<InvalidOperationException>(
                            () => article.IsBeingWatched = true,
                            () => article.IsExpanded = true,
                            () => article.HasBeenRead = true);
                    }
                });
        }

        /// <summary>Tests various scenarios when persistent objects are deleted.</summary>
        [TestMethod]
        public void DeleteTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var group = GetGroupWithArticle(server, cache, GetUniqueString(), GetUniqueString());
                    var host = group.Host;

                    using (var query = group.CreateQueryCache())
                    {
                        var article = query.GetOriginalInboundArticles().First();
                        group.Delete();
                        host.Delete();
                        AssertThrow<InvalidOperationException>(
                            () => host.Delete(),
                            () => this.AddRandomGroup(host),
                            () => group.Delete(),
                            () => article.IsBeingWatched = true,
                            () => article.IsExpanded = false,
                            () => article.HasBeenRead = false,
                            () => article.IsSelected = false);
                    }
                });
        }

        /// <summary>Tests what happens when the <see cref="Cache"/> object is disposed prematurely.</summary>
        [TestMethod]
        public void CacheDisposedTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var group = GetGroupWithArticle(server, cache, GetUniqueString(), GetUniqueString());
                    var host = group.Host;

                    using (var query = group.CreateQueryCache())
                    {
                        var article = query.GetOriginalInboundArticles().First();
                        cache.Dispose();
                        AssertThrow<ObjectDisposedException>(
                            () => this.AddRandomHost(cache),
                            () => cache.GetHosts().GetEnumerator().MoveNext());
                        AssertThrow<InvalidOperationException>(
                            () => host.Delete(),
                            () => host.GetGroups().GetEnumerator().MoveNext(),
                            () => this.AddRandomGroup(host),
                            () => group.Delete(),
                            () => query.GetOriginalInboundArticles().GetEnumerator().MoveNext(),
                            () => article.GetFollowupInboundArticles().GetEnumerator().MoveNext(),
                            () => article.IsBeingWatched = false,
                            () => article.IsExpanded = false,
                            () => article.HasBeenRead = false,
                            () => article.IsSelected = false);
                    }
                });
        }

        /// <summary>Tests what happens when a server responds that the service is unavailable.</summary>
        [TestMethod]
        public void ServiceUnavailableTest()
        {
            TestWithUnavailableFakeServer(400, ServiceUnavailableTest);
            TestWithUnavailableFakeServer(502, ServiceUnavailableTest);
        }

        /// <summary>Tests what happens when a server responds that there is an internal server fault.</summary>
        [TestMethod]
        public void ServerFaultTest()
        {
            var observer = new ConsoleObserver();
            TestFailingReceive(observer, 403, new ServerFaultException());
        }

        /// <summary>Tests what happens when a server responds that a group is unknown.</summary>
        [TestMethod]
        public void GroupUnknownTest()
        {
            var observer = new ConsoleObserver();
            TestFailingReceive(observer, 411, new NoSuchGroupException());
        }

        /// <summary>Tests what happens when a server responds that the client must authenticate itself.</summary>
        [TestMethod]
        public void AuthenticationMissingTest()
        {
            var observer = new ConsoleObserver();
            TestFailingReceive(observer, 480, new MissingAuthenticationException());
        }

        /// <summary>Tests what happens when a server responds that suitable privacy must be established.</summary>
        [TestMethod]
        public void PrivacyMissingTest()
        {
            var observer = new ConsoleObserver();
            TestFailingReceive(observer, 483, new MissingPrivacyException());
        }

        /// <summary>Tests what happens when a server responds with an unexpected response.</summary>
        [TestMethod]
        public void ResponseUnexpectedTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var observer = new ConsoleObserver();
                    QueueFailingGroupCommand(server, 999);
                    Receive(cache, true, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                    observer.AssertCallCount<string>(observer.OnConnected, 1);
                    observer.AssertCallCount(new UnexpectedResponseException(), 1);
                    observer.AssertTotalCallCount(4);
                });
        }

        /// <summary>Tests what happens when there is no network connection.</summary>
        [TestMethod]
        public void NoNetworkConnectionTest()
        {
            using (var cache = CreateNewCache())
            {
                var observer = new ConsoleObserver(true);
                Receive(cache, true, observer);
                WaitForSendReceiveToFinish(cache, observer);
                observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                observer.AssertTotalCallCount(2);
                observer.AssertNoExceptions();
            }
        }

        /// <summary>Tests what happens when a server does not respond to the initial connection attempt.</summary>
        [TestMethod]
        public void HostUnreachableTest()
        {
            using (var cache = CreateNewCache())
            {
                var observer = new ConsoleObserver();
                Receive(cache, true, observer);
                WaitForSendReceiveToFinish(cache, observer);
                observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                observer.AssertCallCount(new HostUnreachableException(), 1);
                observer.AssertTotalCallCount(3);
            }
        }

        /// <summary>Tests what happens when a server does not respond after a connection has been established.
        /// </summary>
        [TestMethod]
        public void ConnectionLostTest()
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    var observer = new ConsoleObserver();
                    Receive(cache, true, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                    observer.AssertCallCount<string>(observer.OnConnected, 1);
                    observer.AssertCallCount(new ConnectionLostException(), 1);
                    observer.AssertTotalCallCount(4);
                });
        }

        /// <summary>Tests what happens when the writing of received data fails.</summary>
        [TestMethod]
        public void WriteFailedTest()
        {
            using (var cache = CreateNewCache())
            {
                var messageId1 = GetUniqueString();
                var subject1 = GetUniqueString();
                var messageId2 = GetUniqueString();
                var subject2 = GetUniqueString();
                string groupDirectory =
                    Path.Combine(Path.Combine(Path.Combine(cache.DataDirectory, "In"), "1"), "0");
                Directory.CreateDirectory(groupDirectory);

                using (var server = NntpFakeServerHelper.MakeAvailableServer(true))
                {
                    QueueSucceedingGroupCommand(server);
                    QueueSucceedingStatCommand(server, 1, messageId1);
                    QueueHeadCommand(server, 1, messageId1, subject1, null);
                    QueueSucceedingNextCommand(server, messageId2);
                    QueueHeadCommand(server, 2, messageId2, subject2, null);
                    var observer = new ConsoleObserver();

                    using (var file = File.Create(Path.Combine(groupDirectory, "2.eml")))
                    {
                        Receive(cache, true, observer);
                        WaitForSendReceiveToFinish(cache, observer);
                    }

                    observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                    observer.AssertCallCount<string>(observer.OnConnected, 1);
                    observer.AssertCallCount(new WriteFailedException(), 1);
                    observer.AssertCallCount<string, int>(observer.OnHeadersProgressChanged, 2);
                    observer.AssertTotalCallCount(6);

                    var group = cache.GetHosts().First().GetGroups().First();
                    var articles = group.GetOriginalInboundArticles(InboundArticleFilter.NullFilter).ToList();
                    Assert.AreEqual(1, articles.Count);
                    Assert.AreEqual(ReceiveStatus.HeaderOnly, articles[0].ReceiveStatus);

                    articles[0].IsBeingWatched = true;
                }

                using (var server = NntpFakeServerHelper.MakeAvailableServer(true))
                {
                    QueueSucceedingGroupCommand(server);
                    QueueSucceedingStatCommand(server, 2, messageId2);
                    QueueHeadCommand(server, 2, messageId2, subject2, null);
                    var response =
                        "222 1 " + messageId1 + "\r\n" +
                        "Whatever" + "\r\n" +
                        ".\r\n";
                    server.QueueCommand("BODY 1\r\n", response);
                    var observer = new ConsoleObserver();

                    using (var file = new FileStream(
                        Path.Combine(groupDirectory, "1.eml"), FileMode.Append, FileAccess.Write, FileShare.None))
                    {
                        cache.StartSendReceive(
                            new IGroup[] { cache.GetHosts().First().GetGroups().First() }, false, observer);
                        WaitForSendReceiveToFinish(cache, observer);
                    }

                    observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                    observer.AssertCallCount<string>(observer.OnConnected, 1);
                    observer.AssertCallCount<string, int>(observer.OnHeadersProgressChanged, 2);
                    observer.AssertCallCount<string, int>(observer.OnBodiesProgressChanged, 1);
                    observer.AssertCallCount(new WriteFailedException(), 1);
                    observer.AssertTotalCallCount(7);
                }
            }
        }

        /// <summary>Tests what happens when the data directory contains incompatible data.</summary>
        [TestMethod]
        public void IncompatibleCacheTest()
        {
            TestIncompatibleCache(false, "DROP TABLE MetaData");
            TestIncompatibleCache(false, "UPDATE MetaData SET Version = 1");
            TestIncompatibleCache(false, "ALTER TABLE MetaData DROP COLUMN Version");
            TestIncompatibleCache(
                false,
                "ALTER TABLE MetaData DROP COLUMN Version",
                "ALTER TABLE MetaData ADD COLUMN Version DATETIME DEFAULT 2001/01/01 NOT NULL");
        }

        /// <summary>Tests all cache related exceptions.</summary>
        [TestMethod]
        public void ExceptionTest()
        {
            TestStdExceptionConstructors<ConnectionLostException>();
            TestStdExceptionConstructors<HostUnreachableException>();
            TestStdExceptionConstructors<InsufficientDiskSpaceException>();
            TestStdExceptionConstructors<IncompatibleCacheException>();
            TestStdExceptionConstructors<MissingComponentException>();
            TestStdExceptionConstructors<WriteFailedException>();
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void AddRandomHosts(Cache cache)
        {
            AssertThrow<ArgumentNullException>(() => cache.AddHost(null, 0));
            AssertThrow<ArgumentException>(() => cache.AddHost(new string('x', cache.HostNameMaxLength + 1), 0));
            AssertThrow<ArgumentOutOfRangeException>(
                () => cache.AddHost(GetUniqueString(), IPEndPoint.MinPort - 1),
                () => cache.AddHost(GetUniqueString(), IPEndPoint.MaxPort + 1));
            AssertThrow<ArgumentException>(() => cache.AddHost(
                GetUniqueString(), 119, new string('x', cache.UserNameMaxLength + 1), null, null, null));
            AssertThrow<ArgumentException>(() => cache.AddHost(
                GetUniqueString(), 119, null, new string('x', cache.EmailAddressMaxLength + 1), null, null));
            AssertThrow<ArgumentException>(() => cache.AddHost(
                GetUniqueString(), 119, null, null, new string('x', cache.AccountNameMaxLength + 1), string.Empty));
            AssertThrow<ArgumentException>(() => cache.AddHost(
                GetUniqueString(), 119, null, null, string.Empty, new string('x', cache.PasswordMaxLength + 1)));

            var hostCount = Random.Next(1, 10);

            for (var i = 0; i < hostCount; ++i)
            {
                this.AddRandomGroups(cache, this.AddRandomHost(cache));
            }

            var firstHost = cache.GetHosts().First();
            AssertThrow<ArgumentException>(() => cache.AddHost(firstHost.Name, firstHost.Port));

            Assert.AreEqual(hostCount, cache.GetHosts().Count());
            cache.GetHosts().First().Delete();
            Assert.AreEqual(hostCount - 1, cache.GetHosts().Count());
        }

        private IHost AddRandomHost(Cache cache)
        {
            var name = GetUniqueString();
            var port = this.GetRandomPort();
            var userName = GetUniqueString();
            var emailAddress = GetUniqueString();
            var accountName = GetUniqueString();
            var password = GetUniqueString();
            var host = cache.AddHost(name, port, userName, emailAddress, accountName, password);
            Assert.AreEqual(name, host.Name);
            Assert.AreEqual(port, host.Port);
            Assert.AreEqual(userName, host.UserName);
            Assert.AreEqual(emailAddress, host.EmailAddress);
            Assert.AreEqual(accountName, host.AccountName);
            Assert.AreEqual(password, host.Password);
            userName = GetUniqueString();
            emailAddress = GetUniqueString();
            accountName = GetUniqueString();
            password = GetUniqueString();
            host.UserName = userName;
            host.EmailAddress = emailAddress;
            host.AccountName = accountName;
            host.Password = password;
            Assert.AreEqual(userName, host.UserName);
            Assert.AreEqual(emailAddress, host.EmailAddress);
            Assert.AreEqual(accountName, host.AccountName);
            Assert.AreEqual(password, host.Password);
            return host;
        }

        private void AddRandomGroups(Cache cache, IHost host)
        {
            AssertThrow<ArgumentNullException>(
                () => host.AddGroup(null, "x", 1, 1),
                () => host.AddGroup("x", null, 1, 1));
            AssertThrow<ArgumentException>(
                () => host.AddGroup(new string('x', cache.GroupNameMaxLength + 1), "x", 1, 1),
                () => host.AddGroup("x", new string('x', cache.GroupDisplayNameMaxLength + 1), 1, 1));
            AssertThrow<ArgumentOutOfRangeException>(
                () => host.AddGroup("x", "x", 0, 1),
                () => host.AddGroup("x", "x", 1, 0));

            var groupCount = Random.Next(1, 10);

            for (int i = 0; i < groupCount; ++i)
            {
                this.AddRandomGroup(host);
            }

            var firstGroup = host.GetGroups().First();
            AssertThrow<ArgumentException>(() => host.AddGroup(firstGroup.Name, firstGroup.DisplayName, 1, 1));

            Assert.AreEqual(groupCount, host.GetGroups().Count());
            host.GetGroups().First().Delete();
            Assert.AreEqual(groupCount - 1, host.GetGroups().Count());
        }

        private IGroup AddRandomGroup(IHost host)
        {
            string name = GetUniqueString();
            string differentName = GetUniqueString();
            int newInboundArticlesReceiveLimit = this.GetRandomNewInboundArticlesReceiveLimit();
            int differentNewInboundArticlesReceiveLimit = newInboundArticlesReceiveLimit + 1;
            int cachedInboundArticlesLimit = this.Random.Next(newInboundArticlesReceiveLimit, int.MaxValue);
            int differentCachedInboundArticlesLimit = cachedInboundArticlesLimit + 1;
            var filter = new InboundArticleFilter(
                this.GetRandomBool(), this.GetRandomBool(), this.GetRandomBool(), this.GetRandomBool());
            var differentFilter = new InboundArticleFilter(
                Shift(filter.IsOriginalBeingWatched),
                Shift(filter.HasOriginalBeenRead),
                Shift(filter.IsFollowupBeingWatched),
                Shift(filter.HasFollowupBeenRead));
            Assert.IsTrue(filter != differentFilter);
            Assert.IsFalse(filter.Equals(null));
            Assert.IsFalse(filter.Equals((object)differentFilter));

            var group = host.AddGroup(name, name, newInboundArticlesReceiveLimit, cachedInboundArticlesLimit);
            group.InboundArticleFilter = filter;
            AssertEqual(group, host.Name, name, name, newInboundArticlesReceiveLimit, cachedInboundArticlesLimit, filter);
            group.DisplayName = differentName;
            AssertThrow<ArgumentNullException>(() => group.DisplayName = null);
            AssertThrow<ArgumentException>(() => group.DisplayName = new string('x', 33));
            group.CachedInboundArticlesLimit = differentCachedInboundArticlesLimit;
            group.NewInboundArticlesReceiveLimit = differentNewInboundArticlesReceiveLimit;
            AssertThrow<ArgumentOutOfRangeException>(
                () => group.CachedInboundArticlesLimit = differentNewInboundArticlesReceiveLimit - 1);
            AssertThrow<ArgumentOutOfRangeException>(
                () => group.NewInboundArticlesReceiveLimit = differentCachedInboundArticlesLimit + 1);
            group.InboundArticleFilter = differentFilter;
            AssertEqual(
                group,
                host.Name,
                name,
                differentName,
                differentNewInboundArticlesReceiveLimit,
                differentCachedInboundArticlesLimit,
                differentFilter);
            return group;
        }

        private int GetRandomPort()
        {
            return this.Random.Next(1, short.MaxValue + 1);
        }

        private int GetRandomNewInboundArticlesReceiveLimit()
        {
            return this.Random.Next(1, int.MaxValue);
        }

        private bool? GetRandomBool()
        {
            switch (this.Random.Next(3))
            {
                case 0:
                    return null;
                case 1:
                    return false;
                default:
                    return true;
            }
        }

        private static Cache CreateNewCache()
        {
            var dataDirectory = Path.Combine(ourDirectory, GetUniqueString());
            Cache.CreateCache(dataDirectory);
            var result = new Cache(dataDirectory, new NullCryptoProvider());
            Assert.AreEqual(dataDirectory, result.DataDirectory);
            Assert.IsTrue(result.HostNameMaxLength > 0);
            Assert.IsTrue(result.UserNameMaxLength > 0);
            Assert.IsTrue(result.EmailAddressMaxLength > 0);
            Assert.IsTrue(result.AccountNameMaxLength > 0);
            Assert.IsTrue(result.PasswordMaxLength > 0);
            Assert.IsTrue(result.GroupNameMaxLength > 0);
            Assert.IsTrue(result.GroupDisplayNameMaxLength > 0);
            Assert.IsTrue(result.ArticleSubjectMaxLength > 0);
            return result;
        }

        private static void EqualsGetHashCodeTest<T>(T object1, T object1Clone, T object2) where T : IEquatable<T>
        {
            Assert.IsTrue(((object)object1).Equals(object1Clone));
            Assert.IsFalse(((object)object1).Equals(new object()));
            Assert.IsTrue(object1.Equals(object1Clone));
            Assert.IsFalse(object1.Equals(null));
            Assert.IsFalse(object1.Equals(object2));
            Assert.AreEqual(object1.GetHashCode(), object1Clone.GetHashCode());
            Assert.AreNotEqual(object1.GetHashCode(), object2.GetHashCode());
        }

        private static void AssertReadStatus(IEnumerable<IInboundArticle> articles, bool read)
        {
            foreach (var article in articles)
            {
                Assert.AreEqual(read, article.HasBeenRead);
                AssertReadStatus(article.GetFollowupInboundArticles(), read);
            }
        }

        private static void TestStartStopSendReceive(Cache cache, IHost host1, IGroup group1, IGroup group3)
        {
            AssertThrow<ArgumentNullException>(
                () => cache.StartSendReceive(null, false, new ConsoleObserver()),
                () => cache.StartSendReceive(new IGroup[] { group1 }, false, null));

            var observer = new ConsoleObserver();

            // Start/stop send/receive
            cache.StartSendReceive(new IGroup[] { group3 }, true, observer);
            Assert.IsTrue(cache.IsSendReceiveInProgress);

            AssertThrow<InvalidOperationException>(
                () => cache.StartSendReceive(new IGroup[] { group3 }, true, observer),
                () => cache.Compact(),
                () => group1.Delete(),
                () => host1.Delete());
            Thread.Sleep(1000); // Ensure that the send/receive thread had a chance to start
            cache.StopSendReceive();
            Assert.IsFalse(cache.IsSendReceiveInProgress);
        }

        private static void TestOutboundArticles(Cache cache, IHost host2, IGroup group2, IGroup group3)
        {
            var outboundArticle = host2.AddOutboundArticle();
            AssertThrow<ArgumentException>(() => outboundArticle.SendStatus = SendStatus.Sent);
            Assert.AreEqual(SendStatus.Draft, outboundArticle.SendStatus);
            AssertOutboundCounts(cache, 1, 0, 0);
            AssertThrow<ArgumentNullException>(() => outboundArticle.SaveMessage(null));

            Assert.AreEqual(host2, outboundArticle.Host);

            var message = CreateTestMessage();
            outboundArticle.SaveMessage(message);
            Assert.IsNotNull(message.Date);
            Assert.IsNotNull(message.MessageId);
            Assert.AreEqual(message.Date, outboundArticle.DateUtc);
            Assert.AreEqual(message.Subject, outboundArticle.Subject);
            var date = message.Date;
            var messageId = message.MessageId;

            Assert.AreEqual(SendStatus.Draft, outboundArticle.SendStatus);
            outboundArticle.SendStatus = SendStatus.Outbox;
            Assert.AreEqual(SendStatus.Outbox, outboundArticle.SendStatus);
            AssertOutboundCounts(cache, 0, 1, 0);

            var loadedMessage = outboundArticle.LoadMessage();
            Assert.IsTrue(TimeSpan.FromSeconds(1) > date - loadedMessage.Date);
            Assert.AreEqual(messageId, loadedMessage.MessageId);
            outboundArticle.SaveMessage(loadedMessage);
            loadedMessage = outboundArticle.LoadMessage();
            Assert.AreEqual(messageId, loadedMessage.MessageId);

            var outboundArticle2 = host2.AddOutboundArticle();
            var message2 = CreateTestMessage();
            message2.Body = null;
            outboundArticle2.SaveMessage(message2);
            outboundArticle2.SendStatus = SendStatus.Outbox;
            AssertOutboundCounts(cache, 0, 2, 0);

            var observer = new ConsoleObserver();
            cache.StartSendReceive(new IGroup[] { group2, group3 }, false, observer);
            WaitForSendReceiveToFinish(cache, observer);
            observer.AssertCallCount(new UnknownCommandException(), 1);
            AssertOutboundCounts(cache, 0, 1, 1);
            var unsentArticle = cache.GetOutboundArticles(SendStatus.Outbox).First();
            unsentArticle.SendStatus = SendStatus.Draft;
            var sentArticle = cache.GetOutboundArticles(SendStatus.Sent).First();
            AssertThrow<InvalidOperationException>(() => sentArticle.SaveMessage(new Message()));
            AssertThrow<InvalidOperationException>(() => sentArticle.SendStatus = SendStatus.Outbox);
            AssertOutboundCounts(cache, 1, 0, 1);
            Assert.IsFalse(unsentArticle.IsDeleted);
            unsentArticle.IsDeleted = true;
            Assert.IsTrue(unsentArticle.IsDeleted);
            sentArticle.IsDeleted = true;
            Assert.AreEqual(2, cache.GetDeletedOutboundArticles().Count());
            AssertOutboundCounts(cache, 0, 0, 0);
            unsentArticle.IsDeleted = false;
            sentArticle.IsDeleted = false;
            AssertOutboundCounts(cache, 1, 0, 1);
        }

        private static void AssertOutboundCounts(Cache cache, int draft, int outbox, int sent)
        {
            Assert.AreEqual(draft, cache.GetOutboundArticles(SendStatus.Draft).Count());
            Assert.AreEqual(outbox, cache.GetOutboundArticles(SendStatus.Outbox).Count());
            Assert.AreEqual(sent, cache.GetOutboundArticles(SendStatus.Sent).Count());
            var counts = cache.GetOutboundArticleCounts();
            Assert.AreEqual(draft, counts[SendStatus.Draft]);
            Assert.AreEqual(outbox, counts[SendStatus.Outbox]);
            Assert.AreEqual(sent, counts[SendStatus.Sent]);
        }

        private static Message CreateTestMessage()
        {
            return new Message()
            {
                Fields = { new UnstructuredHeaderField("Newsgroups") { Body = "alt.test" } },
                From = new List<Address>() { new Address("Andreas Huber", "ahd6974-spamboostorgtrap@yahoo.com") },
                Subject = Guid.NewGuid().ToString("N"),
                Body = new TextBody(TextSubtypeNames.Plain) { Content = "ignore" }
            };
        }

        private static void TestQueryCache(
            int newInboundArticlesReceiveLimit,
            Cache cache,
            IGroup group1,
            IGroup group2,
            IGroup group3)
        {
            // Test query cache
            using (var query2 = group2.CreateQueryCache())
            {
                Assert.AreEqual(group2, query2.Group);
                group2.InboundArticleFilter = new InboundArticleFilter(true, true, true, true);
                Assert.IsTrue(group2.GetThreadCount(new InboundArticleFilter(null, null, null, null)) > 0);
                Assert.AreEqual(0, query2.GetOriginalInboundArticles().Count());
                Assert.AreEqual(0, query2.GetThreadCount());
                group2.InboundArticleFilter = new InboundArticleFilter(null, null, null, null);
                Assert.AreEqual(0, group2.GetThreadCount(new InboundArticleFilter(true, true, true, true)));
                var articles =
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(null, null, null, null)).ToList();
                Assert.IsTrue(query2.GetOriginalInboundArticles().SequenceEqual(articles));
                Assert.AreEqual(articles.Count, query2.GetThreadCount());
            }

            using (var query1 = group1.CreateQueryCache())
            using (var query2 = group2.CreateQueryCache())
            {
                Assert.IsTrue(query2.GetOriginalInboundArticles().SequenceEqual(
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(null, null, null, null))));
                Assert.IsTrue(query2.GetOriginalInboundArticles().SequenceEqual(
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(false, null, false, null))));
                Assert.IsTrue(query2.GetOriginalInboundArticles().SequenceEqual(
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(null, false, null, false))));
                Assert.IsTrue(query2.GetOriginalInboundArticles().SequenceEqual(
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(false, false, false, false))));
                Assert.AreEqual(
                    0, group2.GetOriginalInboundArticles(new InboundArticleFilter(null, true, null, true)).Count());
                Assert.AreEqual(
                    0,
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(false, true, false, true)).Count());
                Assert.AreEqual(
                    0, group2.GetOriginalInboundArticles(new InboundArticleFilter(true, null, true, null)).Count());
                Assert.AreEqual(
                    0,
                    group2.GetOriginalInboundArticles(new InboundArticleFilter(true, false, true, false)).Count());
                Assert.AreEqual(
                    0, group2.GetOriginalInboundArticles(new InboundArticleFilter(true, true, true, true)).Count());
                int group2CountBefore = TestArticles(group2, query2.GetOriginalInboundArticles(), false, false);
                Assert.IsTrue((group2CountBefore > 0) && (group2CountBefore <= newInboundArticlesReceiveLimit));

                // Receive full article content, ensure that previously marked articles are received too
                var observer = new ConsoleObserver();
                cache.StartSendReceive(new IGroup[] { group1, group2, group3 }, true, observer);
                WaitForSendReceiveToFinish(cache, observer);
                observer.AssertNoExceptions();

                int group1Count = TestArticles(group1, query1.GetOriginalInboundArticles(), true, false);
                Assert.IsTrue((group1Count > 0) && (group1Count <= newInboundArticlesReceiveLimit));

                var group2CountAfter = GetArticleCount(group2);

                if (group2CountAfter == group2CountBefore)
                {
                    Assert.AreEqual(
                        group2CountBefore, TestArticles(group2, query2.GetOriginalInboundArticles(), true, true));
                }
                else
                {
                    Assert.IsTrue(group2CountBefore < group2CountAfter);
                }

                TestCompilerGeneratedEnumerator(query1.GetOriginalInboundArticles().GetEnumerator());
            }
        }

        private static void TestCompaction(string dataDirectory, int group1ArticleCount)
        {
            using (var stream = File.Open(
                Path.Combine(dataDirectory, "Cache.sdf"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (var cache = new Cache(dataDirectory, new NullCryptoProvider(), 1))
            {
                Assert.IsTrue(cache.ShouldCompact);
                AssertThrow<IOException>(() => cache.Compact());
                var group = cache.GetHosts().First().GetGroups().First();
                group.NewInboundArticlesReceiveLimit -= 11;
                group.CachedInboundArticlesLimit -= 11;
            }

            using (var cache2 = new Cache(dataDirectory, new NullCryptoProvider(), 1))
            {
                Assert.IsTrue(cache2.ShouldCompact);
                cache2.Compact();
                Assert.IsTrue(GetArticleCount(cache2.GetHosts().First().GetGroups().First()) < group1ArticleCount);
            }

            using (var cache3 = new Cache(dataDirectory, new NullCryptoProvider(), 0))
            {
                Assert.IsFalse(cache3.ShouldCompact);
                cache3.Compact();
            }

            AssertThrow<ArgumentOutOfRangeException>(
                () => new Cache(dataDirectory, new NullCryptoProvider(), -1).Dispose());
        }

        private static IHost CreateGmaneHost(Cache cache)
        {
            return cache.AddHost("news.gmane.org", 119);
        }

        private static IGroup CreateGccDeveloperGroup(
            IHost host, int newInboundArticlesReceiveLimit, int cachedInboundArticlesLimit)
        {
            return host.AddGroup(
                "gmane.comp.gcc.devel", "GCC Developer", newInboundArticlesReceiveLimit, cachedInboundArticlesLimit);
        }

        private static IGroup CreateBoostDeveloperGroup(
            IHost host, int newInboundArticlesReceiveLimit, int cachedInboundArticlesLimit)
        {
            return host.AddGroup(
                "gmane.comp.lib.boost.devel",
                "Boost Developer",
                newInboundArticlesReceiveLimit,
                cachedInboundArticlesLimit);
        }

        private static IHost CreateTestHost(Cache cache)
        {
            TestServerHelper.GetTestServerClient().Dispose();
            return cache.AddHost(
                TestServerHelper.ServerName,
                119,
                "Andreas Huber",
                "ahd6974-spamboostorgtrap@yahoo.com",
                TestServerHelper.AccountName,
                TestServerHelper.Password);
        }

        private static IGroup CreateTestGroup(
            IHost host, int newInboundArticlesReceiveLimit, int cachedInboundArticlesLimit)
        {
            return host.AddGroup("alt.test", "Test", newInboundArticlesReceiveLimit, cachedInboundArticlesLimit);
        }

        private static int TestArticles(
            IGroup group, IEnumerable<IInboundArticle> articles, bool mustBeReceived, bool mustBeExpandedReadWatched)
        {
            int count = 0;
            byte[] buffer = new byte[1024];

            foreach (var article in articles)
            {
                Assert.AreEqual(group.Name, article.Group.Name);
                Assert.IsTrue(article.Number > 0);
                Assert.IsNotNull(article.DateUtc);
                Assert.IsNotNull(article.From);
                Assert.IsNotNull(article.Subject);
                Assert.AreEqual(mustBeReceived, article.ReceiveStatus == ReceiveStatus.FullArticle);
                Assert.AreEqual(mustBeExpandedReadWatched ? false : (bool?)null, article.HasAttachments);
                Assert.AreEqual(mustBeExpandedReadWatched, article.IsBeingWatched);
                Assert.AreEqual(mustBeExpandedReadWatched, article.IsExpanded);
                Assert.AreEqual(mustBeExpandedReadWatched, article.HasBeenRead);
                Assert.IsFalse(article.IsSelected);
                article.IsSelected = true;
                Assert.IsTrue(article.IsSelected);

                using (var content = article.GetContent())
                {
                    int total = 0;
                    int read;

                    while ((read = content.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        total += read;
                    }

                    Assert.IsTrue(total > 0);
                }

                count += TestArticles(
                    group, article.GetFollowupInboundArticles(), mustBeReceived, mustBeExpandedReadWatched);

                article.HasAttachments = false;
                article.IsBeingWatched = true;
                article.IsExpanded = true;
                article.HasBeenRead = true;

                ++count;
            }

            return count;
        }

        private static int GetArticleCount(IGroup group)
        {
            return GetCount(group.GetOriginalInboundArticles(InboundArticleFilter.NullFilter));
        }

        private static int GetCount(IEnumerable<IInboundArticle> articles)
        {
            int count = 0;

            foreach (var article in articles)
            {
                count += GetCount(article.GetFollowupInboundArticles()) + 1;
            }

            return count;
        }

        private static void TestIncompatibleCache(bool sendReceiveOnly, params string[] commandStrings)
        {
            string dataDirectory;

            using (var cache = CreateNewCache())
            {
                var host = CreateTestHost(cache);
                CreateTestGroup(host, 10, 10);
                dataDirectory = cache.DataDirectory;
                var observer = new ConsoleObserver();
                cache.StartSendReceive(cache.GetHosts().First().GetGroups(), true, observer);
                WaitForSendReceiveToFinish(cache, observer);
                var article = host.AddOutboundArticle();
                article.SaveMessage(CreateTestMessage());
                article.SendStatus = SendStatus.Outbox;
            }

            using (var connection = ConnectToCache(dataDirectory))
            {
                // Drop all referential constraints first. This allows us to drop single tables for testing while it
                // doesn't affect the validity of the tests that don't do so.
                const string ConstraintTableNameColumn = "CONSTRAINT_TABLE_NAME";
                const string ConstraintNameColumn = "CONSTRAINT_NAME";
                const string SelectCommandString =
                    "SELECT " + ConstraintTableNameColumn + ", " + ConstraintNameColumn +
                    " FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS";

                using (var constraintsCommand = new SqlCeCommand(SelectCommandString, connection))
                using (var reader = constraintsCommand.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        string dropCommandString = "ALTER TABLE [" + reader[ConstraintTableNameColumn] +
                            "] DROP CONSTRAINT " + reader[ConstraintNameColumn];

                        using (var dropCommand = new SqlCeCommand(dropCommandString, connection))
                        {
                            dropCommand.ExecuteNonQuery();
                        }
                    }
                }

                foreach (var commandString in commandStrings)
                {
                    using (var command = new SqlCeCommand(commandString, connection))
                    {
                        command.ExecuteNonQuery();
                    }
                }
            }

            if (!sendReceiveOnly)
            {
                AssertThrow<IncompatibleCacheException>(() =>
                {
                    using (var cache = new Cache(dataDirectory, new NullCryptoProvider()))
                    {
                        cache.GetHosts().First().AddOutboundArticle();
                        cache.GetHosts().First().GetGroups().First().GetOriginalInboundArticles(
                            InboundArticleFilter.NullFilter).First();
                    }
                });
            }

            AssertThrow<IncompatibleCacheException>(() =>
            {
                using (var cache = new Cache(dataDirectory, new NullCryptoProvider()))
                {
                    CreateGccDeveloperGroup(cache.GetHosts().First(), 10, 10);
                    var observer = new ConsoleObserver();
                    cache.StartSendReceive(cache.GetHosts().First().GetGroups(), true, observer);

                    try
                    {
                        WaitForSendReceiveToFinish(cache, observer);
                    }
                    catch (TargetInvocationException ex)
                    {
                        throw ex.InnerException;
                    }
                }
            });
        }

        private static SqlCeConnection ConnectToCache(string dataDirectory)
        {
            var connection = new SqlCeConnection("Data Source=" + Path.Combine(dataDirectory, "Cache.sdf"));
            connection.Open();
            return connection;
        }

        private static void QueueSucceedingGroupCommand(FakeServer server)
        {
            server.QueueCommand("GROUP " + OurGroupName + "\r\n", "211 2 1 2 our.group\r\n");
        }

        private static void QueueFailingGroupCommand(FakeServer server, int responseCode)
        {
            server.QueueCommand(
                "GROUP our.group\r\n", responseCode.ToString(CultureInfo.InvariantCulture) + " bla bla\r\n");
        }

        private static void QueueSucceedingStatCommand(FakeServer server, int number, string messageId)
        {
            server.QueueCommand("STAT " + number + "\r\n", "223 " + number + " " + messageId + "\r\n");
        }

        private static void QueueFailingStatCommand(FakeServer server, int number)
        {
            server.QueueCommand("STAT " + number + "\r\n", "423 No article with that number\r\n");
        }

        private static void QueueHeadCommand(
            FakeServer server, int number, string messageId, string subject, string inReplyToMessageId)
        {
            var response =
                "221 " + number + " " + messageId + "\r\n" +
                "Subject: " + subject + "\r\n" +
                "Date: 31 Dec 1752 16:59:59 +0000\r\n" + // This is deliberately less than the SQL Server minimal date
                (string.IsNullOrEmpty(inReplyToMessageId) ? null : "In-Reply-To: " + inReplyToMessageId + "\r\n") +
                ".\r\n";
            server.QueueCommand("HEAD\r\n", response);
        }

        private static void QueueSucceedingNextCommand(FakeServer server, string messageId)
        {
            server.QueueCommand(
                "NEXT\r\n",
                "223 2 " + messageId + "\r\n");
        }

        private static void QueueFailingNextCommand(FakeServer server)
        {
            server.QueueCommand(
                "NEXT\r\n",
                "421 No next article in this group\r\n");
        }

        private static IGroup Receive(Cache cache, bool receiveFullArticles, ISendReceiveProgressObserver observer)
        {
            var group = cache.AddHost("localhost", 119).AddGroup(OurGroupName, "Our Group", 100, 100);
            cache.StartSendReceive(new IGroup[] { group }, receiveFullArticles, observer);
            return group;
        }

        private static void WaitForSendReceiveToFinish(Cache cache, ConsoleObserver observer)
        {
            while (cache.IsSendReceiveInProgress)
            {
                Thread.Sleep(500);
            }

            observer.RethrowException();
        }

        private static IGroup GetGroupWithArticle(FakeServer server, Cache cache, string messageId, string subject)
        {
            QueueSucceedingGroupCommand(server);
            QueueSucceedingStatCommand(server, 1, messageId);
            QueueHeadCommand(server, 1, messageId, subject, null);
            QueueFailingNextCommand(server);
            var observer = new ConsoleObserver();
            var group = Receive(cache, false, observer);
            WaitForSendReceiveToFinish(cache, observer);
            return group;
        }

        private static void TestWithFakeServer(Action<FakeServer, Cache> testCallback)
        {
            using (var server = NntpFakeServerHelper.MakeAvailableServer(true))
            using (var cache = CreateNewCache())
            {
                testCallback(server, cache);
            }
        }

        private static void TestWithUnavailableFakeServer(int responseCode, Action<FakeServer, Cache> testCallback)
        {
            using (var server = NntpFakeServerHelper.MakeUnavailableServer(responseCode))
            using (var cache = CreateNewCache())
            {
                testCallback(server, cache);
            }
        }

        private static string CreateUniqueDirectory()
        {
            string directory = Path.Combine(ourDirectory, GetUniqueString());
            Directory.CreateDirectory(directory);
            return directory;
        }

        private static bool? Shift(bool? value)
        {
            switch (value)
            {
                case null:
                    return false;
                case false:
                    return true;
                default:
                    return null;
            }
        }

        private static void AssertEqual(
            IGroup group,
            string hostName,
            string name,
            string displayName,
            int newInboundArticlesReceiveLimit,
            int cachedInboundArticlesLimit,
            InboundArticleFilter filter)
        {
            Assert.AreEqual(hostName, group.Host.Name);
            Assert.AreEqual(name, group.Name);
            Assert.AreEqual(displayName, group.DisplayName);
            Assert.AreEqual(newInboundArticlesReceiveLimit, group.NewInboundArticlesReceiveLimit);
            Assert.AreEqual(cachedInboundArticlesLimit, group.CachedInboundArticlesLimit);
            Assert.AreEqual(filter, group.InboundArticleFilter);
            Assert.AreEqual(filter.GetHashCode(), group.InboundArticleFilter.GetHashCode());
        }

        private static void ServiceUnavailableTest(FakeServer server, Cache cache)
        {
            var observer = new ConsoleObserver();
            Receive(cache, true, observer);
            WaitForSendReceiveToFinish(cache, observer);
            observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
            observer.AssertCallCount(new ServiceUnavailableException(), 1);
            observer.AssertTotalCallCount(3);
        }

        private static void TestFailingReceive(
            ConsoleObserver observer, int responseCode, Exception expectedException)
        {
            TestWithFakeServer(
                (server, cache) =>
                {
                    QueueFailingGroupCommand(server, responseCode);
                    Receive(cache, true, observer);
                    WaitForSendReceiveToFinish(cache, observer);
                    observer.AssertCallCount<string, IDisposable>(observer.OnConnecting, 1);
                    observer.AssertCallCount<string>(observer.OnConnected, 1);
                    observer.AssertCallCount(expectedException, 1);
                    observer.AssertTotalCallCount(4);
                });
        }

        /// <summary>Represents a crypto provider that does not encrypt.</summary>
        private sealed class NullCryptoProvider : ICryptoProvider
        {
            /// <summary>See <see cref="ICryptoProvider.Encrypt"/>.</summary>
            public byte[] Encrypt(byte[] input)
            {
                return input;
            }

            /// <summary>See <see cref="ICryptoProvider.Decrypt"/>.</summary>
            public byte[] Decrypt(byte[] input)
            {
                return input;
            }
        }
    }
}
